package com.crux.ribbon;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.loadbalancer.core.DelegatingServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import reactor.core.publisher.Flux;

import java.util.ArrayList;
import java.util.List;


/**
 * Rule that use the average/percentile response times
 * to assign dynamic "weights" per Server which is then used in
 * the "Weighted Round Robin" fashion.
 * <p>
 * The basic idea for weighted round robin has been obtained from JCS
 * The implementation for choosing the endpoint from the list of endpoints
 * is as follows:Let's assume 4 endpoints:A(wt=10), B(wt=30), C(wt=40),
 * D(wt=20).
 * <p>
 * Using the Random API, generate a random number between 1 and10+30+40+20.
 * Let's assume that the above list is randomized. Based on the weights, we
 * have intervals as follows:
 * <p>
 * 1-----10 (A's weight)
 * <br>
 * 11----40 (A's weight + B's weight)
 * <br>
 * 41----80 (A's weight + B's weight + C's weight)
 * <br>
 * 81----100(A's weight + B's weight + C's weight + C's weight)
 * <p>
 * Here's the psuedo code for deciding where to send the request:
 * <p>
 * if (random_number between 1 &amp; 10) {send request to A;}
 * <br>
 * else if (random_number between 11 &amp; 40) {send request to B;}
 * <br>
 * else if (random_number between 41 &amp; 80) {send request to C;}
 * <br>
 * else if (random_number between 81 &amp; 100) {send request to D;}
 * <p>
 * When there is not enough statistics gathered for the servers, this rule
 * will fall back to use { RoundRobinRule}.
 *
 * @author stonse
 */
@Slf4j
public class WeightedTimeResponseServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier {
    private CruxLoadBalancerLifecycle cruxLoadBalancerLifecycle;
    private DiscoveryClient discoveryClient;

    public WeightedTimeResponseServiceInstanceListSupplier(ServiceInstanceListSupplier delegate) {
        super(delegate);
    }

    @Override
    public Flux<List<ServiceInstance>> get() {
        return delegate.get().map(this::choose);
    }

    private CruxLoadBalancerLifecycle getCruxLoadBalancerLifecycle() {
        if (cruxLoadBalancerLifecycle == null) {
            try {
                cruxLoadBalancerLifecycle = ServiceLocator.getService(CruxLoadBalancerLifecycle.class);
            } catch (Exception e) {
                log.warn("获取CruxLoadBalancerLifecycle失败");
            }

        }
        return cruxLoadBalancerLifecycle;
    }

    private DiscoveryClient getDiscoveryClient() {
        if (discoveryClient == null) {
            try {
                discoveryClient = ServiceLocator.getService(DiscoveryClient.class);
            } catch (Exception e) {
                log.warn("获取discoveryClient失败");
            }

        }
        return discoveryClient;
    }

    private List<ServiceInstance> choose(List<ServiceInstance> serviceInstances) {
        CruxLoadBalancerLifecycle loadBalancerLifecycle = getCruxLoadBalancerLifecycle();
        if (loadBalancerLifecycle == null) {
            return serviceInstances;
        }
        List<ServiceInstance> serviceInstanceList = new ArrayList<>();
        ServiceInstance minResponseTimeInstance = null;
        for (ServiceInstance serviceInstance : serviceInstances) {
            if (minResponseTimeInstance == null) {
                minResponseTimeInstance = serviceInstance;
            }
            long responseTime = loadBalancerLifecycle.getResponseTimeByInstanceId(serviceInstance.getInstanceId());
            long minResponseTime = loadBalancerLifecycle.getResponseTimeByInstanceId(minResponseTimeInstance.getInstanceId());
            if (responseTime < minResponseTime) {
                minResponseTimeInstance = serviceInstance;
            }
        }
        if (minResponseTimeInstance != null) {
            serviceInstanceList.add(minResponseTimeInstance);
        } else {
            DiscoveryClient client = getDiscoveryClient();
            if (client == null) {
                return serviceInstances;
            }
            List<ServiceInstance> instances = client.getInstances(getServiceId());
            return (List<ServiceInstance>) CollectionUtils.retainAll(instances, serviceInstances);
        }
        return serviceInstanceList;
    }


}
